@vvedantb/ui 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +251 -3
- package/package.json +13 -5
- package/shell/src/SidebarResize.tsx +1 -4
- package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
- package/src/ai-elements/activity-question-utils.ts +26 -0
- package/src/ai-elements/activity-question.tsx +159 -0
- package/src/ai-elements/activity-shared.tsx +459 -0
- package/src/ai-elements/activity-step-detail.tsx +115 -0
- package/src/ai-elements/activity-step-label.ts +981 -0
- package/src/ai-elements/activity-tasks-utils.ts +199 -0
- package/src/ai-elements/activity-tasks.tsx +770 -0
- package/src/ai-elements/artifact.tsx +162 -52
- package/src/ai-elements/attachments.tsx +62 -32
- package/src/ai-elements/code-block.tsx +76 -22
- package/src/ai-elements/context.tsx +370 -0
- package/src/ai-elements/conversation.tsx +186 -62
- package/src/ai-elements/file-chip.tsx +53 -0
- package/src/ai-elements/message.tsx +161 -50
- package/src/ai-elements/model-picker-content.tsx +505 -0
- package/src/ai-elements/model-picker-types.ts +56 -0
- package/src/ai-elements/model-picker.tsx +301 -0
- package/src/ai-elements/model-selector.tsx +102 -21
- package/src/ai-elements/plan.tsx +130 -0
- package/src/ai-elements/prompt-input-settings.tsx +144 -0
- package/src/ai-elements/prompt-input-speech.tsx +189 -0
- package/src/ai-elements/prompt-input-traits.tsx +281 -0
- package/src/ai-elements/prompt-input.tsx +320 -134
- package/src/ai-elements/provider-icon.tsx +133 -0
- package/src/ai-elements/queue.tsx +293 -9
- package/src/ai-elements/shimmer.tsx +72 -1
- package/src/ai-elements/simple-model-ladder.tsx +108 -0
- package/src/ai-elements/speech-input.tsx +5 -3
- package/src/ai-elements/test-results.tsx +510 -0
- package/src/ai-elements/trace.tsx +243 -0
- package/src/ai-elements/use-streaming-disclosure.ts +50 -0
- package/src/ai-elements/web-preview.tsx +349 -0
- package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
- package/src/index.ts +66 -12
- package/src/kibo/avatar-stack.tsx +40 -0
- package/src/kibo/contribution-graph.tsx +539 -0
- package/src/kibo/cursor.tsx +72 -0
- package/src/kibo/gantt-features.tsx +439 -0
- package/src/kibo/gantt-header.tsx +181 -0
- package/src/kibo/gantt-provider.tsx +657 -0
- package/src/kibo/gantt-sidebar.tsx +157 -0
- package/src/kibo/gantt-timeline.tsx +154 -0
- package/src/kibo/index.ts +151 -0
- package/src/kibo/kanban.tsx +338 -0
- package/src/kibo/kanbanDropTarget.ts +25 -0
- package/src/kibo/list.tsx +162 -0
- package/src/kibo/theme-switcher.tsx +109 -0
- package/src/kibo/video-player.tsx +237 -0
- package/src/markdown/Markdown.tsx +112 -0
- package/src/markdown/MarkdownBlockquote.tsx +45 -0
- package/src/markdown/MarkdownCodeBlock.tsx +197 -0
- package/src/markdown/MarkdownInlineCode.tsx +40 -0
- package/src/markdown/MarkdownLink.tsx +92 -0
- package/src/markdown/codeFence.ts +57 -0
- package/src/markdown/codeHighlight.ts +138 -0
- package/src/markdown/incrementalBlocks.ts +182 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/markdownClipboard.ts +178 -0
- package/src/markdown/mermaid.ts +155 -0
- package/src/markdown/rehypeGithubAlerts.ts +80 -0
- package/src/motion/presets.ts +24 -0
- package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
- package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
- package/src/ui/_menu-parts.tsx +29 -0
- package/src/ui/accordion.alt.tsx +46 -53
- package/src/ui/accordion.tsx +59 -10
- package/src/ui/avatar.tsx +56 -0
- package/src/ui/badge.tsx +45 -21
- package/src/ui/border-beam.tsx +97 -0
- package/src/ui/button-group.tsx +50 -21
- package/src/ui/button.tsx +67 -45
- package/src/ui/calendar.tsx +48 -5
- package/src/ui/card.tsx +77 -8
- package/src/ui/carousel.tsx +60 -0
- package/src/ui/centered-spinner.tsx +30 -0
- package/src/ui/checkbox.tsx +34 -16
- package/src/ui/clear-input.alt.tsx +33 -75
- package/src/ui/clear-input.tsx +140 -15
- package/src/ui/collapsible.tsx +40 -2
- package/src/ui/command.tsx +143 -70
- package/src/ui/context-menu.tsx +159 -93
- package/src/ui/crossfade-icon.tsx +123 -0
- package/src/ui/dialog.tsx +154 -70
- package/src/ui/dropdown-menu.tsx +177 -117
- package/src/ui/hover-card.tsx +59 -17
- package/src/ui/input-group.alt.tsx +83 -83
- package/src/ui/input-group.tsx +129 -58
- package/src/ui/input.tsx +13 -4
- package/src/ui/label.tsx +11 -8
- package/src/ui/list-row.tsx +232 -0
- package/src/ui/loading-state.tsx +215 -0
- package/src/ui/popover.tsx +56 -23
- package/src/ui/progress.tsx +28 -16
- package/src/ui/refresh-spin-icon.tsx +41 -0
- package/src/ui/search-input.tsx +66 -15
- package/src/ui/select.tsx +171 -100
- package/src/ui/separator.tsx +23 -14
- package/src/ui/slider.tsx +21 -7
- package/src/ui/sonner.tsx +68 -30
- package/src/ui/spinner.tsx +229 -11
- package/src/ui/status-dot.tsx +84 -0
- package/src/ui/surface.tsx +49 -0
- package/src/ui/switch.tsx +29 -15
- package/src/ui/table.tsx +38 -16
- package/src/ui/tabs.tsx +123 -35
- package/src/ui/tabsSliding.ts +33 -1
- package/src/ui/textarea.tsx +13 -4
- package/src/ui/tooltip.tsx +50 -14
- package/src/ui/ui-config.tsx +149 -0
- package/src/utils/cat-color.ts +123 -0
- package/src/utils/dayjs.ts +6 -0
- package/src/utils/gesture.ts +59 -0
- package/src/utils/runtimeVisibility.ts +167 -0
- package/src/utils/sharedClock.ts +96 -0
- package/src/utils/surface-radius.ts +16 -0
- package/src/utils/useDragSensors.ts +58 -0
- package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
- package/tokens/examples/tokens-moved/README.md +20 -0
- package/tokens/examples/tokens-moved/index.html +12 -0
- package/tokens/examples/tokens-moved/main.tsx +6 -0
- package/tokens/examples/tokens-moved/package.json +14 -0
- package/tokens/examples/tokens-moved/styles.css +12 -0
- package/tokens/examples/tokens-moved/tsconfig.json +14 -0
- package/tokens/examples/tokens-moved/vite.config.ts +11 -0
- package/tokens/src/css/border-beam.css +116 -0
- package/tokens/src/css/markdown.css +318 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Shared React 19 web UI. Source-exported: no build step.
|
|
|
7
7
|
| `@vvedantb/ui` | The only JavaScript entry. Primitives (Radix-based), AI elements, audio (`playChime`, `useReplyChime`, `playTone`), `toast` and `SonnerToaster`, the composer (`InputArea`, deprecated 0.3 `Composer`), shell (`SidebarHeader`, `PillTabs`, `SessionsHoverCard`, and the app shell: `AppShell`, `AppSidebar`, `SidebarRail`, `RailTile`, `SidebarPanel`, `SidebarAccountMenu`, `SignOutDialog` and related helpers), app helpers (confirm skip, update watcher, settings chrome, `useStableAuth`, and from 0.8.0 `createThemeProvider`, `createPageTitleContext`, `PageMotionProvider` and `formatCompactRelativeTime`), marketing pieces from 0.9.0 (`createLandingKit` and the `Landing*` parts, `SiteFooter`, `PricingModal`), animated nav icons from 0.11.0 (`MemoryIcon`, `BoardIcon`, ... and `NavIconBase`; `SettingsIcon` `variant` from 0.12.0), artifact viewers from 0.13.0 (`ArtifactFrame`, `ArtifactList`, `ArtifactViewer`, ...), date pickers from 0.14.0 (`Calendar`, `DatePicker`, `DateTimePicker`, `MonthPicker`), `cn`, and the native tokens `tokens` and `Tokens`. See `app/README.md` in the repository for the app helpers. |
|
|
8
8
|
| `@vvedantb/ui/tokens` | `tokens.css`: colour, radius, shadow, ease and font tokens for Tailwind v4. CSS. |
|
|
9
9
|
| `@vvedantb/ui/tokens/tokens.css` | Same file as `@vvedantb/ui/tokens`. CSS. |
|
|
10
|
-
| `@vvedantb/ui/tokens/css/*.css` | `index`, `base`, `dialog`, `modal`, `tabs`, `tabs-pills`, `utilities`, `scrollbar-thin`, `clear-input`, `motion`, `animations`, `nav-icons`, `compat-shadcn-rgb
|
|
10
|
+
| `@vvedantb/ui/tokens/css/*.css` | `index`, `base`, `dialog`, `modal`, `tabs`, `tabs-pills`, `utilities`, `scrollbar-thin`, `clear-input`, `motion`, `animations`, `nav-icons`, `compat-shadcn-rgb`, `border-beam` (opt-in, for `BorderBeam`), `markdown` (opt-in, for `Markdown`). CSS. `tokens.css` imports `scrollbar-thin`, `clear-input` and `motion`. |
|
|
11
11
|
| `@vvedantb/ui/tokens/compat/shadcn-rgb.css` | Same file as `tokens/css/compat-shadcn-rgb.css`. CSS. |
|
|
12
12
|
| `@vvedantb/ui/tokens/tailwind-preset` | Tailwind preset (`.mjs`) for `@config "@vvedantb/ui/tokens/tailwind-preset"`. Kept as a build-tool entry: Tailwind needs a resolvable package path, and it is not a React API. |
|
|
13
13
|
| `@vvedantb/ui/landing.css` | Landing page classes (`landing-atmosphere`, `landing-grain`, `landing-pulse-dot`, ambient graph and preview animations, smooth `#top` scrolling). CSS. |
|
|
@@ -1039,7 +1039,7 @@ import { SonnerToaster, toast } from "@vvedantb/ui";
|
|
|
1039
1039
|
/>;
|
|
1040
1040
|
```
|
|
1041
1041
|
|
|
1042
|
-
With no new props the toaster looks as it did in 0.2.1. Caller props merge with the package defaults instead of replacing them:
|
|
1042
|
+
With no new props and no `UiConfig.SonnerToaster` entry the toaster looks as it did in 0.2.1. An entry sets app defaults for any prop (see "App config: AI chat" below). Caller props merge with the package defaults instead of replacing them:
|
|
1043
1043
|
|
|
1044
1044
|
- `toastOptions.classNames`: per key, the package classes come first and the caller classes follow, so callers can override utilities. Other `toastOptions` fields (such as `duration`) take the caller value.
|
|
1045
1045
|
- `icons`: merged per key. With `toastIcons="filled24"` the caller overrides single icons and keeps the rest.
|
|
@@ -1063,9 +1063,255 @@ With `closeButtonStyle="hoverChip"`, hover changes the text colour, and sonner m
|
|
|
1063
1063
|
|
|
1064
1064
|
Helpers: `createToasterClassNames`, `mergeToastClassNames`, `mergeToastOptions`, `mergeToasterStyle`, `filledToastIcons`. Fixture: `tokens/examples/toaster/`.
|
|
1065
1065
|
|
|
1066
|
+
## App config
|
|
1067
|
+
|
|
1068
|
+
`UiConfigProvider` gives components app-level defaults, so an app does not pass the same props at every call site. Wrap the app once:
|
|
1069
|
+
|
|
1070
|
+
```tsx
|
|
1071
|
+
import { UiConfigProvider, type UiConfig } from "@vvedantb/ui";
|
|
1072
|
+
|
|
1073
|
+
const config = { cn: appCn } satisfies UiConfig;
|
|
1074
|
+
|
|
1075
|
+
<UiConfigProvider config={config}>{app}</UiConfigProvider>
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
Defaults are unchanged. Without a provider, or with an empty entry, every component renders its built-in DOM. `src/default-dom.test.tsx` renders 53 primitives and AI elements without a provider and compares `document.body` (portals included) with `tokens/test/fixtures/default-dom.json`, byte for byte.
|
|
1079
|
+
|
|
1080
|
+
- Root exports: `UiConfigProvider` (`config`, `children`), `useUiConfig(name)` (the app's entry for one component, or `undefined`) and the `UiConfig` and `UiConfigProviderProps` types.
|
|
1081
|
+
- An entry's `classes` replace built-in classes part by part. A part left out keeps its default, and a caller `className` still merges after it. Other keys are default prop values. An explicit prop always wins.
|
|
1082
|
+
- `cn`: the class merger that components use in place of the built-in `cn` when they merge a caller `className`. Set it when the app's tailwind-merge setup differs, for example when it does not treat `rounded-surface` as a radius class and relies on the CSS order instead. Components read it with the internal `useCn` hook.
|
|
1083
|
+
- Component entries and opt-in props are in the sections below: "App config: existing primitives", "Loaders, rows, status, sortable list and helpers", "App config: AI chat", "Agent activity, plan, tests, preview and model picker", "Kibo blocks".
|
|
1084
|
+
|
|
1085
|
+
## App config: existing primitives
|
|
1086
|
+
|
|
1087
|
+
`UiConfig` entries and opt-in props for components that were already in the package. Without an entry or a new prop, each one renders its built-in DOM. Every component in this section merges a caller `className` with `UiConfig.cn` when it is set.
|
|
1088
|
+
|
|
1089
|
+
### Controls
|
|
1090
|
+
|
|
1091
|
+
```tsx
|
|
1092
|
+
const config = {
|
|
1093
|
+
Button: { classes: { variant: { default: "bg-brand text-white" }, size: { xs: "h-7 px-2 text-2xs" } } },
|
|
1094
|
+
Tabs: { variant: "segmented", pillMotion: "xy" },
|
|
1095
|
+
ClearInput: { animation: "none" },
|
|
1096
|
+
} satisfies UiConfig;
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
- `Button`: `classes.base`, `classes.variant.<variant>`, `classes.size.<size>`. New size `xs` (`h-7 rounded-md px-2 text-xs hit-target`, 14px icons).
|
|
1100
|
+
- `Badge`: `classes.base`, `classes.variant.<variant>`.
|
|
1101
|
+
- `Input`, `Textarea`: `classes.base`.
|
|
1102
|
+
- `Checkbox`: `classes.root`, `classes.indicator`, `icon`. New prop `icon`: the indicator content (default a 14px check). The `CheckboxProps` type is exported.
|
|
1103
|
+
- `Switch`: `classes.root`, `classes.thumb`.
|
|
1104
|
+
- `Slider`: `classes.root`, `classes.track`, `classes.range`, `classes.thumb`. `trackClassName`, `rangeClassName` and `thumbClassName` still merge after them. `children` now render inside the track, after the range (for example tick marks). Before, they were dropped.
|
|
1105
|
+
- `ButtonGroup`: `classes.base`, `classes.orientation.<horizontal|vertical>`, `classes.text` (`ButtonGroupText`), `classes.separator` (`ButtonGroupSeparator`).
|
|
1106
|
+
- `InputGroup`: `classes.root`, `classes.addon.base`, `classes.addon.align.<align>`, `classes.button.base`, `classes.button.size.<size>`, `classes.text`, `classes.input`, `classes.textarea`. New prop `classes` on `InputGroupAddon` (`{ addon }`), `InputGroupText` (`{ text }`), `InputGroupInput` (`{ input }`) and `InputGroupTextarea` (`{ textarea }`): the same keys for one instance; they win over the entry.
|
|
1107
|
+
- `ClearInput`: `animation`, `classes.wrapper`, `classes.input`, `classes.button`. New prop `animation`: `"dissolve"` (default) or `"none"`, which clears at once, focuses the input and renders only the wrapper (`relative`), the input and the clear button. The `dissolve` wrapper keeps its `t-clear` classes and adds `classes.wrapper`. New prop `classes`: the same keys for one instance, each wins over the entry. New prop `clearButton` (with `"none"`): `"filled"` (default) mounts the button while there is a value; `"always"` keeps it mounted, hidden while empty (`pointer-events-none opacity-0`, `tabIndex=-1`, `aria-hidden`), and the input always reserves `pr-9`.
|
|
1108
|
+
- `SearchInput`: `classes.wrapper` (a base for the wrapper), `classes.input` (replaces the size classes), `classes.icon` (replaces the icon classes), `iconSize` (default 16), `label`, `placeholder`, `clearLabel`, `enterKeyHint` (`false` leaves `aria-label` or `enterkeyhint` off), and `classNameTarget: "wrapper"` to send `className` to the wrapper instead of the input. New props: `onClear` (called after `onChange` when the value becomes empty) and `inputClassName` (merged into the input last).
|
|
1109
|
+
- `Calendar`: `classes.base` replaces the root `className` classes; every other key replaces that DayPicker `classNames` key (a caller `classNames` still wins). `components` is the default for the prop, applied over the built-in `Root` and `Chevron`. New export `CalendarDayButton`: DayPicker's day button plus `data-selected`, for classes such as `data-[selected=true]:bg-primary` on the button; pass it as `components.DayButton`.
|
|
1110
|
+
- `Carousel`: `classes.dots`, `classes.dot`, `classes.dotActive`, `classes.dotInactive` for the new `CarouselDots` (one button per snap, nothing for a single page). `useCarousel` is exported and its context adds `selectedIndex`, `scrollSnaps` and `scrollTo`.
|
|
1111
|
+
- `Tabs`: `variant` (default for the `TabsList` prop), `pillMotion` (`"x"` default; `"xy"` moves the segmented pill on both axes with `transform`, for lists that wrap or run in a column), `classes.list` (replaces the list classes of every variant), `classes.size.<default|sm>`, `classes.pill`, `classes.trigger` (every trigger), `classes.content`. New `TabsList` prop `size`: `"sm"` shrinks the triggers (`[&_.t-tab]:px-2.5 py-1 text-xs`); `"default"` adds no classes. New `TabsBar`: a tab strip on a bottom hairline (`border-b border-border`, `px-3 py-2`; `size="sm"` `px-2 py-1.5`) with an `actions` slot on the right. `syncTabsPill` takes an optional motion (`"x"` or `"xy"`).
|
|
1112
|
+
- `Accordion`: `classes.item`, `classes.header`, `classes.trigger`, `classes.content` (the animated panel), `classes.contentInner` (the padded box inside it), `icon`. New `AccordionTrigger` prop `icon`: shown after the children (default the chevron). New prop `classes` on `AccordionItem` (`{ item }`), `AccordionTrigger` (`{ header, trigger }`) and `AccordionContent` (`{ content, contentInner }`): the same keys for one instance; they win over the entry.
|
|
1113
|
+
- `Collapsible`: `classes.trigger`, `classes.content`. The parts have no built-in classes, so without an entry `className` passes through untouched. `CollapsibleTrigger` and `CollapsibleContent` are now thin wrappers (same DOM, refs still reach the element); the `CollapsibleContentProps` type is exported.
|
|
1114
|
+
- Deprecated `AccordionAlt*`, `InputGroupAlt*` and `ClearInputAlt` wrap the main parts with their old markup (pinned by `src/ui/alt-wrappers.test.tsx`; no `UiConfig` entry reaches them). Use the main components; the `Alt` names go in a later minor release once consumers move. `InputGroupAlt` (the root) and `InputGroupButtonAlt` stay a plain `<div>` and a `Button`: the root renders `data-input-group` before `class`, and the button has no default `type`, which the main parts cannot reproduce.
|
|
1115
|
+
|
|
1116
|
+
### Overlays
|
|
1117
|
+
|
|
1118
|
+
```tsx
|
|
1119
|
+
<UiConfigProvider config={{ Dialog: { viewportAware: false, classes: { content: "..." } }, Tooltip: { delayDuration: 300, portal: true } }}>
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
| Entry | `classes` parts | Other keys |
|
|
1123
|
+
| --- | --- | --- |
|
|
1124
|
+
| `Dialog` | `trigger`, `overlay`, `content` (both layouts), `centered`, `compact`, `close`, `header`, `body`, `footer`, `title`, `description` | `viewportAware` |
|
|
1125
|
+
| `Popover` | `trigger`, `content` | `sideOffset`, `collisionPadding` |
|
|
1126
|
+
| `HoverCard` | `content` | `openDelay`, `closeDelay`, `sideOffset`, `portal` |
|
|
1127
|
+
| `Tooltip` | `content` | `delayDuration` (read by `TooltipProvider`), `sideOffset`, `portal` |
|
|
1128
|
+
| `DropdownMenu` | `trigger`, `content`, `subContent`, `subTrigger`, `item`, `checkboxItem`, `radioItem`, `label`, `separator`, `shortcut` | `icons.check`, `icons.radio`, `sideOffset`, `collisionPadding` |
|
|
1129
|
+
| `ContextMenu` | same as `DropdownMenu` | `icons.check`, `icons.radio`, `collisionPadding`, `subContentPortal` |
|
|
1130
|
+
| `Select` | `trigger`, `scrollButton`, `content`, `viewport`, `popperViewport`, `label`, `item`, `separator` | `icons.trigger`, `icons.scrollUp`, `icons.scrollDown`, `icons.check`, `triggerSlot`, `collisionPadding` |
|
|
1131
|
+
| `Command` | `root`, `dialogContent`, `dialogCommand`, `inputWrapper`, `input`, `list`, `empty`, `group`, `separator`, `item`, `shortcut` | `icons.search`, `emptyClassNameReplaces` |
|
|
1132
|
+
|
|
1133
|
+
`icons.*` take a node and replace the built-in icon. A `trigger` class is added only when set: without it the trigger renders exactly as the Radix one.
|
|
1134
|
+
|
|
1135
|
+
New props and exports:
|
|
1136
|
+
|
|
1137
|
+
- `DialogContent` `viewportAware` (default `true`): the bottom sheet on compact viewports, visual-viewport sizing, keyboard-aware scrolling and the body pointer-events fix. `false` keeps a centred dialog at every size and leaves the viewport and body alone.
|
|
1138
|
+
- `PopoverAnchor` (Radix `Popover.Anchor`).
|
|
1139
|
+
- `HoverCardContent` and `TooltipContent` `portal`: render into `document.body`.
|
|
1140
|
+
- `ContextMenuSubContent` `portal`.
|
|
1141
|
+
- Config `Select.triggerSlot`: adds `data-slot="select-trigger"`, which the `ButtonGroup` selectors read.
|
|
1142
|
+
- Config `Command.emptyClassNameReplaces`: a `className` on `CommandEmpty` (even `""`) replaces its classes instead of merging.
|
|
1143
|
+
|
|
1144
|
+
`DialogTrigger`, `PopoverTrigger`, `DropdownMenuTrigger` and `ContextMenuTrigger` are now thin wrappers that read the config; `HoverCard` and `TooltipProvider` are function components that read the configured delays. Same DOM and props without config.
|
|
1145
|
+
|
|
1146
|
+
### Card, table, progress, separator and label
|
|
1147
|
+
|
|
1148
|
+
| Entry | Keys |
|
|
1149
|
+
| --- | --- |
|
|
1150
|
+
| `Card` | `classes.root`, `header`, `title`, `description`, `content`, `footer`, `action`. |
|
|
1151
|
+
| `Table` | `classes.wrapper` (the scroll `<div>`), `table`, `header`, `body`, `footer`, `row`, `head`, `cell`, `caption`. |
|
|
1152
|
+
| `Progress` | `classes.root`, `indicator`. |
|
|
1153
|
+
| `Separator` | `classes.root` (the orientation size classes stay). |
|
|
1154
|
+
| `Label` | `classes.root`. |
|
|
1155
|
+
|
|
1156
|
+
```tsx
|
|
1157
|
+
<UiConfigProvider config={{ Card: { classes: { root: "rounded-surface bg-card text-card-foreground" } } }}>
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
New exports: `CardTitle`, `CardDescription`, `CardFooter`, `CardAction`.
|
|
1161
|
+
|
|
1162
|
+
## Loaders, rows, status, sortable list and helpers
|
|
1163
|
+
|
|
1164
|
+
Basic components and helpers that came from an app, all exported from the root, with `UiConfig` entries for the ones that have app-specific looks. Existing components keep their defaults. Every component here merges a caller `className` with `UiConfig.cn` when it is set.
|
|
1165
|
+
|
|
1166
|
+
### Components
|
|
1167
|
+
|
|
1168
|
+
- `Spinner` `variant?: "icon" | "circle" | "ring"`: without it the spinner draws the `mark` (prop, else `UiConfig.Spinner.mark`), else the loader icon as before; an explicit `variant` wins over the configured mark. `"icon"` is the loader icon. `"circle"` is a faint ring with a two-tone arc (Web Animations, paused off screen), for slots too small for a mark; `colors` (`{ track?, arc?, lead? }`, default `UiConfig.Spinner.circle`, else `currentColor`) sets its strokes. `"ring"` is the `Button` loading ring (fixed `h-4 w-4`, `currentColor`, unlabelled; `size`, `tone`, `stroke` and `decorative` do not apply). `Button loading` now renders `<Spinner variant="ring" />`, with the same DOM as before.
|
|
1169
|
+
- `Spinner` `mark?: SpinnerMark` (`{ viewBox, shapes, strokeWidth, dashArray }`; each shape is `{ points, stroke }` (a `<polygon>`) or `{ d, stroke }` (a `<path>`)). With a mark the spinner draws the outline with a dash that runs around each shape (Web Animations, 2s a lap, paused off screen and in hidden tabs). `tone` and `stroke` do not apply; `decorative` and numeric `size` (as `width`/`height`) do.
|
|
1170
|
+
- `CenteredSpinner` (`label`): a `Spinner` centred in its box (`flex-1`, `p-6`, `aria-busy`).
|
|
1171
|
+
- `LoadingState` (`variant` `Drive | Dots | Orbit`, `size` `default | sm`, `iconOnly`, `label`, `colors`, `labelStyle`): a 3×3 pulsing grid with a label and an elapsed timer (one shared 100ms clock, asleep in hidden tabs).
|
|
1172
|
+
- `RefreshSpinIcon` (`busy`, `className`): a refresh glyph that crossfades to `<Spinner variant="circle" />`.
|
|
1173
|
+
- `CrossfadeIcon` (`show`, `whenTrue`, `whenFalse`, keys, `variant` `default | soft`) and `CrossfadeIconSlot` (`iconKey`): keyed icon swaps on `motion` `m` (needs the app's `LazyMotion`).
|
|
1174
|
+
- `BorderBeam` (`active`, `size`, `colorVariant` `mono | progress | colorful`): a light that travels around the wrapper's border. Import `@vvedantb/ui/tokens/css/border-beam.css` for the `.beam-*` rules; colours are `--beam-color`, `--beam-progress-color`, `--beam-colorful-from` and `--beam-colorful-to`.
|
|
1175
|
+
- `StatusDot` (`tone`, `size`): a small coloured status dot; forwards `ref`.
|
|
1176
|
+
- `Surface` (`density` `tight | default | comfortable | none`) and `surfaceVariants`: a card-filled box with one padding.
|
|
1177
|
+
- `ListRow` (`link`, `href`, `onClick`, `selected`, `accentClassName`, `contentClassName`, `decoration`) and `LIST_ROW_CONTROL_CLASS`: a selectable row with a stretched native overlay; nested controls take `LIST_ROW_CONTROL_CLASS` to sit above it.
|
|
1178
|
+
- `Avatar`, `AvatarImage`, `AvatarFallback` (Radix Avatar from `radix-ui`).
|
|
1179
|
+
- Sortable list (built on [Kibo UI](https://www.kibo-ui.com) and dnd-kit): `SortableListProvider` (`onDragEnd` gets a `SortableListDragEndEvent`, vertical drag), `SortableListGroup`, `SortableListHeader` (`name` + `color`, or `children`), `SortableListItems`, `SortableListItem`. For a static selectable row use `ListRow`.
|
|
1180
|
+
- `useDragSensors({ sortable? })`: the dnd-kit sensor set for drag surfaces (mouse arms on distance, touch on a 200ms hold, keyboard; `sortable` adds sortable keyboard coordinates).
|
|
1181
|
+
|
|
1182
|
+
`Avatar`, the `ListRow` idle hover and the sortable list drop highlight fill with `surface-secondary` (the same colour as `muted` under `compat-shadcn-rgb.css`).
|
|
1183
|
+
|
|
1184
|
+
New dependencies (installed with the package): `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/modifiers` and `@dnd-kit/utilities`.
|
|
1185
|
+
|
|
1186
|
+
### App config entries
|
|
1187
|
+
|
|
1188
|
+
| Entry | Keys |
|
|
1189
|
+
| --- | --- |
|
|
1190
|
+
| `Spinner` | `mark` (default for the `mark` prop), `circle` (`{ track?, arc?, lead? }`, default for `colors` of the `circle` variant). |
|
|
1191
|
+
| `LoadingState` | `label`, `colors` (three column colours), `labelStyle`, `classes.label`. |
|
|
1192
|
+
| `StatusDot` | `classes.tone` (fill class per tone; tones left out keep theirs). |
|
|
1193
|
+
| `ListRow` | `classes.root`, `interactive`, `selected`, `idle`, `accent`, `overlay`, `content`. |
|
|
1194
|
+
|
|
1195
|
+
```tsx
|
|
1196
|
+
<UiConfigProvider
|
|
1197
|
+
config={{
|
|
1198
|
+
Spinner: {
|
|
1199
|
+
mark: { viewBox: "0 0 24 24", strokeWidth: 2, dashArray: "0.3 0.7", shapes: [{ d: LOGO_PATH, stroke: "#635bff" }] },
|
|
1200
|
+
circle: { track: "#635bff", arc: "#635bff", lead: "#00d4ff" },
|
|
1201
|
+
},
|
|
1202
|
+
}}
|
|
1203
|
+
>
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
### Helpers
|
|
1207
|
+
|
|
1208
|
+
- Motion (in the motion presets): `tokenTransition.fast` (150ms), `tokenTransition.base` (220ms), `tokenTransition.slow` (320ms), all `motionEase` like the CSS `--motion-fast/base/slow` tokens, and `staggerDelay(index, step = motionTiming.stagger, max = 0.2)`. The spring is the existing `springTransition`.
|
|
1209
|
+
- Gesture physics: `rubberband(overshoot, dimension, constant = 0.55)`, `projectVelocity(velocity, decelerationRate = 0.998)`, `DRAG_ACTIVATION_DISTANCE_PX` (8). `SidebarResize` now uses the shared `rubberband` (same maths).
|
|
1210
|
+
- Animation budget: `ensureRuntimeVisibility()` (sets `html[data-page-hidden]`), `observeAnimVisibility(el, onChange?)` and `useAnimOffscreenRef()` (set `data-anim-offscreen`), `bindRuntimeAnimation(el, animation)` (pauses a WAAPI loop while hidden, off screen or in a parked `aria-hidden` subtree; decorative chrome marked `data-anim-chrome` does not count).
|
|
1211
|
+
- Shared clock: `subscribeQuantized(intervalMs, onChange)`, `quantizedSnapshot(intervalMs)`, `msUntilNextBoundary(intervalMs)`.
|
|
1212
|
+
- Identity colours: `catSlotForId`, `catColorForId`, `catColorForSlot`, `CAT_SLOT_COUNT` (class names `text-cat-N`, `bg-cat-N`, `bg-cat-N/12`, `border-cat-N`; the app defines the `cat` colours).
|
|
1213
|
+
- Radius classes: `SURFACE_RADIUS_CLASS` (`rounded-surface`), `CONTROL_RADIUS_CLASS` (`rounded-control`).
|
|
1214
|
+
|
|
1215
|
+
## App config: AI chat
|
|
1216
|
+
|
|
1217
|
+
`UiConfigProvider` entries and opt-in props for the AI chat elements, plus the components and markdown renderer that came from an app. Without an entry each element renders its built-in DOM; `classes` parts replace built-in classes and the caller `className` merges last (with `UiConfig.cn`); other keys are prop defaults, and an explicit prop wins.
|
|
1218
|
+
|
|
1219
|
+
```tsx
|
|
1220
|
+
const config = {
|
|
1221
|
+
Conversation: { scrollButton: { label: "Scroll to end", showDelay: 150, size: "sm" } },
|
|
1222
|
+
Message: { lazyActionTooltips: true },
|
|
1223
|
+
PromptInput: { classes: { groupDragOver: "border-primary bg-primary/5" }, submitCrossfade: true },
|
|
1224
|
+
Shimmer: { mode: "mask" },
|
|
1225
|
+
CodeBlock: { plain: true, copyLabel: "Copy code" },
|
|
1226
|
+
} satisfies UiConfig;
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
- `Conversation`: `classes.content` (`ConversationContent`), `classes.scroll` (a default `scrollClassName`, merged before the prop), `classes.download`, and for `ConversationScrollButton` `classes.scrollButton`, `classes.scrollButtonMotion` (the animated wrapper; `""` renders it without a class) and `classes.scrollButtonFrame` (a static wrapper around the presence; none by default). `scrollButton` holds defaults for `label`, `icon`, `showDelay`, `size` and the motion (`initial`, `animate`, `exit`, `transition`). New `ConversationScrollButton` props: `label` (text after the icon, also `aria-label` and `title`), `icon`, `showDelay` (ms to wait after leaving the bottom before showing; hiding stays immediate; default 0) and `resetKey` (a change hides the button and cancels a pending show). The button now animates with `m` inside its own `LazyMotion`, so it also works under an app's `<LazyMotion strict>`.
|
|
1230
|
+
- `Message`: `classes.root`, `classes.user`, `classes.assistant` (the role classes of `Message`), `classes.content` (all `MessageContent` classes), `classes.branchSelector`, `classes.branchIcon` (a class in place of the arrows' `size={14}`), and `lazyActionTooltips`: a `MessageAction` mounts its tooltip on first hover or focus (focus opens it), not with the button.
|
|
1231
|
+
- `PromptInput`: `classes.group` (the input group, default `overflow-hidden`), `classes.groupIdle` / `classes.groupDragOver` (added while no drag / an accepted drag is over the form), `classes.groupDefault` (added when no `containerClassName` is given), `classes.textarea`, `classes.footer`, `classes.submit`, `classes.tabItem`; `submitIcons.<send|submitted|streaming|error>`, `submitCrossfade` (cross-fade the submit icon on status change) and `menuTriggerAsChild` (`PromptInputActionMenuTrigger` becomes the Radix trigger instead of a button inside it). New `PromptInput` props: `dataDrop` (`{ type, onDrop }`: a non-file drag of that MIME type counts as a drag over the form and `onDrop` gets its string payload) and `inputGroupClassName` (same as `containerClassName`). The attachments context adds `replace(id, file)`; `accept` now matches extension patterns such as `.html` by file name; `useOptionalPromptInputController` is exported.
|
|
1232
|
+
- `PromptInputSettings` (new): an overflow menu with a "Model" submenu (one radio list, or a submenu per provider). Entry: `providerLabel(provider)` (default the id), `providerIcon(provider)` (default none), `classes.trigger`.
|
|
1233
|
+
- `TraitsPanel` (new, with `isUltrathinkPrompt`, `applyUltrathinkPrefix`): reasoning, speed, context and thinking segments for a model picker; inside a `PromptInputProvider` it offers "Ultrathink" by editing the prompt. Entry: `classes.label`, `classes.empty`, `classes.option`, `classes.optionSelected`, `classes.optionIdle`, `classes.note`.
|
|
1234
|
+
- `PromptInputSpeech`, `useSpeechRecognition`, `getSpeechRecognition` (new): Web Speech API dictation; the button renders nothing without the API.
|
|
1235
|
+
- `Queue`: one family, used in one of two ways. The queued-messages widget is unchanged: `Queue` with `count`, `QueueHeader`, `QueueList` (`open`) and `QueueItem` with `text` (`model`, `personalityName`, `personaName`, `personaImage`, `onEdit`, `onRemove`). The composable parts (the AI Elements `Queue`, new): `Queue` without `count` (a plain root), `QueueSection`, `QueueSectionTrigger`, `QueueSectionLabel`, `QueueSectionContent`, `QueueList` (inside a composable `Queue`: a list capped at `max-h-48`), `QueueItem` without `text` (an `li`, takes `ref`), `QueueItemIndicator`, `QueueItemContent`, `QueueItemActions`, `QueueItemAction`. The composable parts render no `motion.*`, so they work under `<LazyMotion strict>`. Entry `Queue`: `classes.sectionTrigger`.
|
|
1236
|
+
- `Shimmer`: `mode`, `as`, `classes.base`, `classes.overlay`, `classes.band`, `classes.copy`. New prop `mode`: `"gradient"` (default) or `"mask"`, which sweeps a masked copy of the text with transforms only (no repaint, paused offscreen). `mask` needs the app to define `.shimmer-overlay`, `.shimmer-band` and `.shimmer-copy` (or the `classes` it names).
|
|
1237
|
+
- `CodeBlock`: `plain`, `copyButtonSize`, `copyLabel` (a screen-reader label on the copy button), `copyEveryClick` (the copy button copies on every click, also while it shows the check; by default those clicks do nothing until it resets after `timeout`), `classes.container`, `classes.pre`, `classes.copyButton`, `classes.copyIcon` (a class in place of `size={14}`). New props: `plain` (no shiki: a `pre` with the code, and `children` replace that `pre` instead of sitting above it); `language` is now optional.
|
|
1238
|
+
- `Artifact`: `classes.root`, `classes.header`, `classes.title`, `classes.description`, `classes.actions`, `classes.action` (`ArtifactAction` and `ArtifactClose`), `classes.actionIcon`, `classes.content`, `actionSize`, `actionLabel` (`"sr-only"` names actions with a hidden span instead of `aria-label`). `ArtifactAction`: `tooltip` is optional (no tooltip, no wrapper) and `icon` also takes an icon component (`icon={IconCopy}`). New `ArtifactClose` (an action with an X, `label` default `"Close"`).
|
|
1239
|
+
- `Attachments`: `classes.item.<variant>`, `classes.preview.<variant>`, `classes.remove.<variant>` replace the variant classes of `Attachment`, `AttachmentPreview` and `AttachmentRemove`.
|
|
1240
|
+
- `FileChip` (new, no entry): a file name chip that opens the file.
|
|
1241
|
+
- `SonnerToaster`: `UiConfig.SonnerToaster` sets a default for any `SonnerToaster` prop; an explicit prop wins, and a prop passed as `undefined` keeps the config value. New prop `classes` (`ToastClassNames & { toaster?: string }`): replaces the package `classNames` per key (`""` removes one); `toaster` replaces `toaster group !font-sans` on the list (`""` removes it). Caller `toastOptions.classNames` still merge after. See also "Toaster" above.
|
|
1242
|
+
|
|
1243
|
+
### Markdown
|
|
1244
|
+
|
|
1245
|
+
`Markdown` (Streamdown with GFM, KaTeX math, CJK, Shiki code blocks with a wrap toggle and copy button, mermaid diagrams loaded on first use, GitHub alerts `> [!NOTE]`, links with the site favicon, file paths in inline code, copy as markdown) and `MarkdownLink`. `components` and `remarkPlugins` extend the defaults. It imports KaTeX's stylesheet; import `@vvedantb/ui/tokens/css/markdown.css` for the typography. That file reads the shadcn channel variables (`rgb(var(--foreground))`, `--muted-foreground`, `--border`, `--primary`, `--success`, `--warning`, `--destructive`), as with `compat-shadcn-rgb.css`, and fills code block and table panels with `var(--color-surface-secondary)` (defined by `tokens.css` and by the compat file, which maps it to the app's `--muted`); `--markdown-alert-important` (RGB channels, default `var(--primary)`) colours `[!IMPORTANT]`. `Markdown` merges a caller `className` with `UiConfig.cn`.
|
|
1246
|
+
|
|
1247
|
+
New dependency (installed with the package): `katex`. `streamdown` and the `@streamdown/*` plugins stay peers; `streamdown` must be `^2.3.0` (`Markdown` uses `useIsCodeFenceIncomplete`, `parseMarkdownIntoBlocks` and the plugin types, which 2.2 and earlier lack).
|
|
1248
|
+
|
|
1249
|
+
## Agent activity, plan, tests, preview and model picker
|
|
1250
|
+
|
|
1251
|
+
AI surfaces that came from an app, exported from the root. Fills use `surface-secondary` (`bg-surface-secondary`, `from-surface-secondary/40`), which is the app's `--muted` under `compat-shadcn-rgb.css` and the secondary surface under `tokens.css`. They also use the shadcn colour names `bg-secondary`, `bg-popover` and `bg-card` and a few app utilities (`motion-press`, `smooth-shadow-*`, `scrollbar`, `scrollbar-none`, `text-2xs`, `text-3xs`), so they look as intended with `compat-shadcn-rgb.css` and an app that defines those utilities. They merge a caller `className` with `UiConfig.cn` when set.
|
|
1252
|
+
|
|
1253
|
+
- `ActivityTasks` (`steps`, `isStreaming`, `name`, `startedAt`, `duration`, `layout` `folded | inline`, `onOpenFile`, `onRequestFullDetail`): an agent turn as a timeline. Tool runs fold behind one summary line, reasoning and mid-turn text render as Markdown, subtasks nest their steps, long timelines cap behind "Show N more". `OpenSubagentContext` gives subtask rows a "View" link.
|
|
1254
|
+
- The step model: `ActivityStep`, `ActivityTodoItem`, `ActivityQuestion`, `ActivityQuestionOption`, `ActivityStepOutput`, `ActivityStepEdit`, `activityStepHasRichDetail`, `activityStepConfig` (glyph and fallback label per step type), `ActivityThinkingIcon`, `useActivitySpinnerVerb`, `getRandomActivityVerb`, `useActivityElapsedSeconds`, `formatActivityElapsed`, `useStreamingDisclosure`.
|
|
1255
|
+
- Tool-call details show files with `FileChip` and output in a `CodeBlock` (`plain`) with its copy button, so the `CodeBlock` entry (see "App config: AI chat") styles those frames too.
|
|
1256
|
+
- `ActivityQuestionCard` (`questions`, `answers`): the read-only record of a multiple-choice question and its answer.
|
|
1257
|
+
- `AgentTrace`, `AgentTraceLine` (`status`: `AgentTraceStatus`, `running | error | pending`), `AgentTraceIcon` (`iconKey` crossfades a swap), `AgentTraceTitle` (`shimmer`), `AgentTraceDescription`, `AgentTraceCollapsible`, `AgentTraceCollapsibleTrigger` (`trailing`: controls beside the trigger), `AgentTraceCollapsibleContent`: one agent step as one line, with an optional fold.
|
|
1258
|
+
- `AgentPlan*`, `TestResults*` / `TestSuite*` / `TestResult*` (one test: `TestResult`, `TestResultStatusIndicator`, `TestResultName`, `TestResultDuration`, `TestResultError`, `TestResultErrorMessage`, `TestResultErrorStack`), `WebPreview*` (with `useWebPreview`), `ContextUsage*` (context window usage in a hover card: `ContextUsage`, `ContextUsageTrigger`, `ContextUsageContent`, `ContextUsageContentHeader` / `Body` / `Footer`, `ContextUsageInput`, `ContextUsageOutput`, `ContextUsageCacheRead`, `ContextUsageCacheWrite`, deprecated `ContextUsageCache`).
|
|
1259
|
+
- `ModelPicker` (button and popover; `simpleLadder` opens on a discrete slider; the one model picker), `ModelPickerContent` (the searchable model and account list, for menus; pair it with `modelPickerSurfaceClass`), `ModelOption`, `ModelAccount`, `findModelOption`, `formatModelDisplayLabel`, `getProviderLabel`, `ProviderIcon` (`claude`, `codex`, `opencode`, `cursor`).
|
|
1260
|
+
- Deprecated, removed in the next minor release: `ModelSelector*` and `PromptInputModelSelect*` (use `ModelPicker`). Behaviour is unchanged, except `ModelSelectorLogo`: it now renders `ProviderIcon` and fetches nothing (it loaded `https://models.dev/logos/<provider>.svg`). `anthropic`, `google-vertex-anthropic` and `claude` show the Claude mark, `openai` and `codex` the OpenAI mark, `opencode` and `cursor` their marks; any other id shows a neutral CPU glyph. It takes SVG props and `size` (default 12) and keeps the `"<provider> logo"` name. `ModelSelectorLogoGroup` styles `svg` children.
|
|
1261
|
+
|
|
1262
|
+
### App config entries
|
|
1263
|
+
|
|
1264
|
+
| Entry | Keys |
|
|
1265
|
+
| --- | --- |
|
|
1266
|
+
| `ActivityTasks` | `name` (agent name in the streaming header, default `"Agent"`; the `name` prop wins), `markdown` (a component that takes `{ className, mode, children }` and renders reasoning and reply text; default `Streamdown` with the cjk, code, math and mermaid plugins), `thinkingIcon` (`{ src, alt }`: `ActivityThinkingIcon` shows this image, 16px and round, instead of the brain glyph). |
|
|
1267
|
+
| `ActivityQuestionCard` | `classes.selectedOption` (fill of the picked option, default `bg-surface-tertiary`). |
|
|
1268
|
+
| `ModelPicker` | `classes.trigger` (replaces the `ModelPicker` trigger classes), `badgeRing.trigger` / `badgeRing.panel` (ring colour around the account initials badge on the trigger and in the panel; default `var(--color-border)` / `var(--color-surface-secondary)`). |
|
|
1269
|
+
|
|
1270
|
+
```tsx
|
|
1271
|
+
const config = {
|
|
1272
|
+
ActivityTasks: { name: "Ada", markdown: AppMarkdown, thinkingIcon: { src: "/logo.svg", alt: "Ada" } },
|
|
1273
|
+
ActivityQuestionCard: { classes: { selectedOption: "bg-accent" } },
|
|
1274
|
+
ModelPicker: { badgeRing: { trigger: "rgb(var(--input))", panel: "rgb(var(--muted))" } },
|
|
1275
|
+
} satisfies UiConfig;
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
## Kibo blocks
|
|
1279
|
+
|
|
1280
|
+
Blocks that came from an app, built on [Kibo UI](https://www.kibo-ui.com). All exported from the root, next to the sortable list.
|
|
1281
|
+
|
|
1282
|
+
### New components
|
|
1283
|
+
|
|
1284
|
+
- Gantt: `GanttProvider` (`range`: `GanttRange`, `daily | monthly | quarterly`, `zoom`, `onZoomChange` for Ctrl/Cmd + wheel, `onAddItem`), `GanttSidebar` (`headerTitle`, `headerMeta`), `GanttSidebarGroup`, `GanttSidebarItem` (`icon`, `meta`, `selected`, `onSelectItem`), `GanttSidebarHeader`, `GanttTimeline`, `GanttHeader`, `GanttContentHeader`, `GanttColumns`, `GanttColumn`, `GanttFeatureList`, `GanttFeatureListGroup`, `GanttFeatureRow`, `GanttFeatureItem` (`onMove`, `onClick`), `GanttFeatureItemCard`, `GanttFeatureDragHelper`, `GanttToday`, `GanttContext` / `useGanttContext`. The canvas pans on drag, centres on today and grows by a year at either edge.
|
|
1285
|
+
- Kanban: `KanbanProvider` (`columns`, `data`, `overlay`, drag callbacks, `collisionDetection`), `KanbanBoard` (`disabled`; highlights while a card is over it or over one of its cards), `KanbanHeader`, `KanbanCards` (render prop per item), `KanbanCard`, `useKanbanContext`.
|
|
1286
|
+
- `ContributionGraph` (`data`: `ContributionGraphActivity[]`, `labels`: `ContributionGraphLabels`) with `ContributionGraphCalendar`, `ContributionGraphBlock`, `ContributionGraphFooter`, `ContributionGraphTotalCount`, `ContributionGraphLegend`.
|
|
1287
|
+
- `VideoPlayer` (media-chrome `MediaController`, dark control styling) with `VideoPlayerContent`, `VideoPlayerControlBar` and the `VideoPlayer*Button`, `VideoPlayerTimeRange`, `VideoPlayerTimeDisplay` and `VideoPlayerVolumeRange` controls.
|
|
1288
|
+
- `LiveCursor`, `LiveCursorPointer`, `LiveCursorBody`, `LiveCursorName` (a collaborator's pointer and name tag); `AvatarStack` (`size`); `ThemeSwitcher` (`value`, `defaultValue`, `onChange`; system, light, dark).
|
|
1289
|
+
|
|
1290
|
+
Kanban, the sortable list and the gantt bars drag with `useDragSensors`.
|
|
1291
|
+
|
|
1292
|
+
New dependencies (installed with the package): `media-chrome` and `dayjs` (a dev dependency before).
|
|
1293
|
+
|
|
1294
|
+
### App config entries
|
|
1295
|
+
|
|
1296
|
+
| Entry | Keys |
|
|
1297
|
+
| --- | --- |
|
|
1298
|
+
| `Gantt` | `classes.root` (scroll container), `today` (today pill), `todayDate` (the date under it), `dragHandle` (resize grip), `offscreenIndicator` (edge date chip). |
|
|
1299
|
+
| `Kanban` | `classes.card` (`KanbanCard` cursor, transitions and press), `cardTransition` (CSS `transition` list appended to dnd-kit's inline reorder transition; default 150ms `scale` and `opacity`). |
|
|
1300
|
+
| `ThemeSwitcher` | `classes.button` (each theme button). |
|
|
1301
|
+
| `VideoPlayer` | `classes.root` (`VideoPlayer`), `classes.controlBar` (`VideoPlayerControlBar`). |
|
|
1302
|
+
|
|
1303
|
+
The defaults use plain Tailwind (`duration-150`, `text-[0.625rem]`, `shadow-lg`, `[scrollbar-width:none]`). Fills use `surface-secondary` (with `compat-shadcn-rgb.css` that is `--muted`). Apps with their own motion or shadow utilities put them here. Every kibo part merges a caller `className` with `UiConfig.cn`.
|
|
1304
|
+
|
|
1305
|
+
```tsx
|
|
1306
|
+
const config = {
|
|
1307
|
+
Kanban: { cardTransition: "scale var(--motion-fast) ease-out, opacity var(--motion-fast) ease-out" },
|
|
1308
|
+
VideoPlayer: { classes: { root: "overflow-hidden rounded-surface shadow-xl" } },
|
|
1309
|
+
} satisfies UiConfig;
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1066
1312
|
## Colour names in components
|
|
1067
1313
|
|
|
1068
|
-
Components use `text-muted-foreground`, `primary` and `--color-*` theme variables. They no longer use `text-muted`, `*-accent` or raw `var(--muted)` reads, which clash with shadcn names. Exceptions that match existing app markup on purpose: the `InputArea` ghost text, the `Button` `flat` variant and the `CommandDialog` `listLayout="auto"` group headings use `text-muted`. `tokens.css` still defines `--color-muted` and `--color-accent`. `pnpm --filter @vvedantb/ui check:tokens` fails if a component brings the old names back (it also runs in `build`, `test` and `verify`).
|
|
1314
|
+
Components use `text-muted-foreground`, `primary` and `--color-*` theme variables. They no longer use `text-muted`, `*-accent` or raw `var(--muted)` reads, which clash with shadcn names. Exceptions that match existing app markup on purpose: the `InputArea` ghost text, the `Button` `flat` variant and the `CommandDialog` `listLayout="auto"` group headings use `text-muted`. `tokens.css` still defines `--color-muted` and `--color-accent`. `pnpm --filter @vvedantb/ui check:tokens` fails if a component brings the old names back (it also runs in `build`, `test` and `verify`). It also fails on fill-type `muted` (`bg-`, `from-`, `via-`, `to-`, `fill-`, `stroke-`, `border-`, `ring-`, `divide-` and `outline-muted`, with variants and opacity; not `*-muted-foreground`): `muted` is a text colour in `tokens.css` and a fill in shadcn, so components fill with `surface-secondary`, which is the same fill under `compat-shadcn-rgb.css`. The dark-mode `Switch` off state, the `Slider` track and thumb and the slash menu row hover keep their `muted` fills on purpose, each line marked `check-tokens-allow: muted`. The package CSS (`tokens/src/css` and any `.css` under `src`) is checked too: a background, fill, stroke, border or outline colour, shadow or `--*-bg`-style variable that reads `--muted` or `--color-muted` fails; fill with `var(--color-surface-secondary)`.
|
|
1069
1315
|
|
|
1070
1316
|
## Using an existing shadcn RGB theme
|
|
1071
1317
|
|
|
@@ -1089,6 +1335,8 @@ The compat file defines `.glass-panel-strong` for toasts, dialogs and hover card
|
|
|
1089
1335
|
|
|
1090
1336
|
A fixture that renders the main components against sample light and dark RGB variables is in `tokens/examples/compat-shadcn-rgb/`.
|
|
1091
1337
|
|
|
1338
|
+
The components that came from the app (loaders, `ListRow`, `Surface`, `BorderBeam`, `Avatar`, `StatusDot`, kanban and the sortable list, activity, agent trace, agent plan, test results, the model picker and markdown) render on plain `tokens.css` in light and dark in `tokens/examples/tokens-moved/`.
|
|
1339
|
+
|
|
1092
1340
|
`@clerk/clerk-react`, `convex`, `@tanstack/react-hotkeys`, `motion`, `zod` and `@vvedantb/server` (0.13.0) are optional peers (`peerDependenciesMeta`), so the package manager does not install or warn about them. The root still imports each of them statically from some module (`useStableAuth` and `ConvexClerkProvider` import Clerk; `ConvexClerkProvider` and `useEnsureUser` import `convex/react`; the artifact viewers import `@vvedantb/server`; the motion presets, `ConfirmSkipHint` and others import the rest). So an app that imports `@vvedantb/ui` must have all six resolvable, even if it uses none of those exports: a bundler resolves every import before it tree-shakes. `app/tests-node/convex-esm.test.mjs` pins which root modules import `convex` and Clerk, and `app/tests-node/artifacts-esm.test.mjs` which import `@vvedantb/server`.
|
|
1093
1341
|
|
|
1094
1342
|
Full documentation: the repository `README.md`. Upgrading from `@vvedantb/tokens`, `@vvedantb/shell` or `@vvedantb/app-kit`: `MIGRATION.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared React 19 UI for web apps: one JavaScript entry (primitives, shell pieces, app helpers including the Convex and Clerk provider, VITE env parsing and ensure-user, artifact viewers over the @vvedantb/server artifacts core, native tokens), design-token CSS, shared Tailwind v4 CSS and a Tailwind preset.",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"./tokens/css/animations.css": "./tokens/src/css/animations.css",
|
|
33
33
|
"./tokens/css/nav-icons.css": "./tokens/src/css/nav-icons.css",
|
|
34
34
|
"./tokens/css/compat-shadcn-rgb.css": "./tokens/src/css/compat-shadcn-rgb.css",
|
|
35
|
+
"./tokens/css/border-beam.css": "./tokens/src/css/border-beam.css",
|
|
36
|
+
"./tokens/css/markdown.css": "./tokens/src/css/markdown.css",
|
|
35
37
|
"./tokens/compat/shadcn-rgb.css": "./tokens/src/css/compat-shadcn-rgb.css",
|
|
36
38
|
"./tokens/css/*.css": "./tokens/src/css/*.css",
|
|
37
39
|
"./tokens/tailwind-preset": {
|
|
@@ -58,6 +60,13 @@
|
|
|
58
60
|
"./src/marketing/landing.css"
|
|
59
61
|
],
|
|
60
62
|
"dependencies": {
|
|
63
|
+
"@dnd-kit/core": "^6.3.1",
|
|
64
|
+
"@dnd-kit/modifiers": "^9.0.0",
|
|
65
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
66
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
67
|
+
"dayjs": "^1.11.13",
|
|
68
|
+
"katex": "^0.16.45",
|
|
69
|
+
"media-chrome": "^4.14.0",
|
|
61
70
|
"react-day-picker": "^9.14.0"
|
|
62
71
|
},
|
|
63
72
|
"peerDependencies": {
|
|
@@ -102,7 +111,7 @@
|
|
|
102
111
|
"react-dom": ">=19.0.0",
|
|
103
112
|
"shiki": "^3.0.0",
|
|
104
113
|
"sonner": "^2.0.3",
|
|
105
|
-
"streamdown": "
|
|
114
|
+
"streamdown": "^2.3.0",
|
|
106
115
|
"tailwind-merge": "^2.6.0",
|
|
107
116
|
"use-stick-to-bottom": "^1.1.0",
|
|
108
117
|
"vaul": "^1.1.0",
|
|
@@ -144,7 +153,6 @@
|
|
|
144
153
|
"clsx": "^2.1.1",
|
|
145
154
|
"cmdk": "^1.1.1",
|
|
146
155
|
"convex": "^1.47.0",
|
|
147
|
-
"dayjs": "1.11.19",
|
|
148
156
|
"embla-carousel-react": "^8.6.0",
|
|
149
157
|
"happy-dom": "^18.0.0",
|
|
150
158
|
"motion": "^12.0.0",
|
|
@@ -155,7 +163,7 @@
|
|
|
155
163
|
"react-dom": "^19.1.0",
|
|
156
164
|
"shiki": "^3.0.0",
|
|
157
165
|
"sonner": "^2.0.3",
|
|
158
|
-
"streamdown": "
|
|
166
|
+
"streamdown": "^2.3.0",
|
|
159
167
|
"tailwind-merge": "^2.6.0",
|
|
160
168
|
"tailwindcss": "4.3.3",
|
|
161
169
|
"typescript": "^5.9.0",
|
|
@@ -189,7 +197,7 @@
|
|
|
189
197
|
"scripts": {
|
|
190
198
|
"check:tokens": "node tokens/scripts/check-component-tokens.mjs",
|
|
191
199
|
"build": "node tokens/scripts/check-exports.mjs && node tokens/scripts/check-component-tokens.mjs && tsc -p tsconfig.json",
|
|
192
|
-
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.consumer.json && tsc -p tokens/tsconfig.json",
|
|
200
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.consumer.json && tsc -p tsconfig.consumer-speech.json && tsc -p tokens/tsconfig.json",
|
|
193
201
|
"test": "node tokens/scripts/check-component-tokens.mjs && vitest run && node --test tokens/test/*.test.mjs app/tests-node/*.test.mjs",
|
|
194
202
|
"verify": "node tokens/scripts/check-component-tokens.mjs && node tokens/verify/run.mjs",
|
|
195
203
|
"analyze": "node tokens/verify/analyze.mjs"
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "react";
|
|
11
11
|
import { animate } from "motion";
|
|
12
12
|
import { cn } from "../../src/utils/cn";
|
|
13
|
+
import { rubberband } from "../../src/utils/gesture";
|
|
13
14
|
|
|
14
15
|
/** Opt-in resizable sidebar panel. Widths are panel px, rail excluded. */
|
|
15
16
|
export type SidebarResizeOptions = {
|
|
@@ -45,10 +46,6 @@ export const SidebarResizeProvider = SidebarResizeContext.Provider;
|
|
|
45
46
|
|
|
46
47
|
const KEYBOARD_STEP = 16;
|
|
47
48
|
|
|
48
|
-
function rubberband(overshoot: number, dimension: number, constant = 0.55): number {
|
|
49
|
-
return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
49
|
type Sample = { x: number; t: number };
|
|
53
50
|
|
|
54
51
|
function releaseVelocity(samples: Sample[]): number {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`vibot Queue widget DOM > renders the closed widget 1`] = `"<div class="w-full overflow-hidden" style="height: auto; opacity: 1;"><button type="button" class="flex items-center justify-between w-full px-4 py-2.5 bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-tertiary rounded-t-2xl group"><div class="flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-stack-2 text-muted-foreground"><path d="M12 4l-8 4l8 4l8 -4l-8 -4"></path><path d="M4 12l8 4l8 -4"></path><path d="M4 16l8 4l8 -4"></path></svg><span class="text-sm font-medium text-foreground">Queued Messages</span><div class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-default text-default-foreground text-xs">3</div></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-chevron-down text-muted-foreground transition-transform duration-200 ease-out"><path d="M6 9l6 6l6 -6"></path></svg></button></div>"`;
|
|
4
|
+
|
|
5
|
+
exports[`vibot Queue widget DOM > renders the editing state, then saves the trimmed text 1`] = `"<div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex flex-col gap-2"><textarea class="flex w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2.5 sm:text-sm placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-hidden focus-visible:border-focus-border focus-visible:ring-1 focus-visible:ring-focus-ring min-h-[60px] text-sm">Draft</textarea><div class="flex justify-end gap-2"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground rounded-lg text-xs max-sm:hit-target h-7 px-2"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-x"><path d="M18 6l-12 12"></path><path d="M6 6l12 12"></path></svg><span class="ml-1 text-xs">Cancel</span></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:-translate-y-0.5 hover:opacity-90 rounded-lg text-xs max-sm:hit-target h-7 px-2"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-check"><path d="M5 12l5 5l10 -10"></path></svg><span class="ml-1 text-xs">Save</span></button></div></div></div>"`;
|
|
6
|
+
|
|
7
|
+
exports[`vibot Queue widget DOM > renders the editing state, then saves the trimmed text 2`] = `"<div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Draft</p><div class="flex items-center gap-1.5 shrink-0"></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div>"`;
|
|
8
|
+
|
|
9
|
+
exports[`vibot Queue widget DOM > renders the header with a custom class 1`] = `"<button type="button" class="flex items-center justify-between w-full px-4 py-2.5 bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-tertiary rounded-t-2xl group x"><div class="flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-stack-2 text-muted-foreground"><path d="M12 4l-8 4l8 4l8 -4l-8 -4"></path><path d="M4 12l8 4l8 -4"></path><path d="M4 16l8 4l8 -4"></path></svg><span class="text-sm font-medium text-foreground">Queued Messages</span><div class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-default text-default-foreground text-xs">1</div></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-chevron-down text-muted-foreground transition-transform duration-200 ease-out"><path d="M6 9l6 6l6 -6"></path></svg></button>"`;
|
|
10
|
+
|
|
11
|
+
exports[`vibot Queue widget DOM > renders the open widget 1`] = `"<div class="w-full overflow-hidden mb-2" style="height: auto; opacity: 1;"><button type="button" class="flex items-center justify-between w-full px-4 py-2.5 bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-tertiary rounded-t-2xl group"><div class="flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-stack-2 text-muted-foreground"><path d="M12 4l-8 4l8 4l8 -4l-8 -4"></path><path d="M4 12l8 4l8 -4"></path><path d="M4 16l8 4l8 -4"></path></svg><span class="text-sm font-medium text-foreground">Queued Messages</span><div class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-default text-default-foreground text-xs">3</div></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-chevron-down text-muted-foreground transition-transform duration-200 ease-out rotate-180"><path d="M6 9l6 6l6 -6"></path></svg></button><div class="overflow-hidden" style="height: auto; opacity: 1;"><div class="bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none rounded-b-2xl"><div dir="ltr" data-slot="scroll-area" class="relative max-h-[240px]" style="position: relative; --radix-scroll-area-corner-width: 0px; --radix-scroll-area-corner-height: 0px;"><style>[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}</style><div data-radix-scroll-area-viewport="" data-slot="scroll-area-viewport" class="size-full rounded-[inherit] outline-hidden" style="overflow-x: hidden; overflow-y: scroll;"><div style="min-width: 100%; display: table;"><div class="p-2 space-y-1.5"><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><img alt="Ada" class="size-5 rounded-full shrink-0" src="https://example.com/ada.png"><p class="text-sm text-foreground/90 flex-1 truncate">Fix the build</p><div class="flex items-center gap-1.5 shrink-0"><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">claude-sonnet</div><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">Terse</div></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary extra"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Plain</p><div class="flex items-center gap-1.5 shrink-0"><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">Bob</div></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Bare</p><div class="flex items-center gap-1.5 shrink-0"></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div></div></div></div></div></div></div></div>"`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ActivityQuestion } from "./activity-shared";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The offered labels the user picked. A whole-answer match wins first so a
|
|
5
|
+
* single-select label containing ", " is not mistaken for several picks; only
|
|
6
|
+
* a multi-select answer is split on the ", " the dock joins it with.
|
|
7
|
+
*/
|
|
8
|
+
export function selectedLabels(
|
|
9
|
+
answer: string | undefined,
|
|
10
|
+
question: ActivityQuestion,
|
|
11
|
+
): string[] {
|
|
12
|
+
if (answer === undefined) return [];
|
|
13
|
+
const offered = new Set(question.options.map((option) => option.label));
|
|
14
|
+
if (offered.has(answer)) return [answer];
|
|
15
|
+
if (!question.multiSelect) return [];
|
|
16
|
+
return answer.split(", ").filter((label) => offered.has(label));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Free text typed into "Other": an answer that matches no offered label. */
|
|
20
|
+
export function freeTextAnswer(
|
|
21
|
+
answer: string | undefined,
|
|
22
|
+
chosen: string[],
|
|
23
|
+
): string | null {
|
|
24
|
+
if (answer === undefined || answer.trim().length === 0) return null;
|
|
25
|
+
return chosen.length > 0 ? null : answer;
|
|
26
|
+
}
|